%%%-------------------------------------------------------------------
%%% @author Viktor Green <viktor@student.gu.se>
%%% @copyright (C) 2011, Viktor Green
%%% @doc
%%% Gen server that handles interval between tracker connections
%%% and keeps a list of the peers the tracker returned as handling
%%% this torrent
%%% @end
%%% Created : 11 Nov 2011 by Viktor Green <viktor@student.gu.se>
%%%-------------------------------------------------------------------
-module(tracker_srv).

-behaviour(gen_server).

%% API
-export([start_link/2, start_link/3, is_connected/1, get_peers/1, stop/1, get_info_hash/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).

-define(SERVER, ?MODULE).
-define(DEFAULT_INTERVAL, 1800). %% 30 minutes

-include("records.hrl").

-record(state, {torrent :: #torrent{}
	       ,minimal_interval = none :: none | timer:tref()
	       ,interval :: timer:tref()
	       ,connected = false :: boolean()
	       ,peers = [] :: [tracker:address()]
	       ,left = 0 :: integer()
	       ,controller :: pid() | undefined}).

-ifdef(debug).
-define(LOG(X), io:format("[~p:~p:~p] - {~p,~p}: ~p~n",
			  tuple_to_list(time()) ++ [?MODULE,
						    ?LINE, X])).
-else.
-define(LOG(X), true).
-endif.

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @end
%%--------------------------------------------------------------------
-spec start_link(Torrent :: #torrent{}, Left :: integer()) ->
			{ok, Pid :: pid()} |
			ignore |
			{error, Error :: term()}.
start_link(Torrent, Left) ->
    start_link(Torrent, Left, undefined).

-spec start_link(Torrent :: #torrent{}, Left :: integer(),
		 Ctrl :: pid() | undefined) ->
			{ok, Pid :: pid()} |
			ignore |
			{error, Error :: term()}.
start_link(Torrent, Left, Ctrl) ->
    gen_server:start_link(?MODULE, [Torrent, Left, Ctrl], []).
%%--------------------------------------------------------------------
%% @doc
%% API call to check if the connection to the tracker has been done
%% successfully
%% @end
%%--------------------------------------------------------------------
-spec is_connected(Pid :: pid()) -> boolean().
is_connected(Pid) ->
    gen_server:call(Pid, is_connected).

%%--------------------------------------------------------------------
%% @doc
%% API call to request the peers for a torrent
%% @end
%%--------------------------------------------------------------------
-spec get_peers(Pid :: pid()) -> [tracker:address()].
get_peers(Pid) ->
    gen_server:call(Pid, get_peers).

%%--------------------------------------------------------------------
%% @doc
%% API call to stop the tracking of a torrent
%% @end
%%--------------------------------------------------------------------
-spec stop(Pid :: pid()) -> ok.
stop(Pid) ->
    gen_server:cast(Pid, stop).

%%--------------------------------------------------------------------
%% @doc
%% API call to get the info_hash of the current torrent
%% @end
%%--------------------------------------------------------------------
-spec get_info_hash(Pid :: pid()) -> string().
get_info_hash(Pid) ->
    gen_server:call(Pid, get_info_hash).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([Torrent, Left, Ctrl]) ->
    ?LOG(initializing_tracker_server),
    {ok, #state{torrent = Torrent, left = Left, controller = Ctrl,
		interval = interval(2, "started")}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%%                                   {reply, Reply, State} |
%%                                   {reply, Reply, State, Timeout} |
%%                                   {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, Reply, State} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call(is_connected, _From, #state{connected = Con} = State) ->
    {reply, Con, State};
handle_call(get_peers, _From, #state{peers = Peers} = State) ->
    {reply, Peers, State};
handle_call(stop, _From, State) ->
    io:format("stopping"),
    {stop, shutdown, ok, State};
handle_call(get_info_hash, _From,
	    #state{torrent = #torrent{info_hash = InfoHash}} = State) ->
    {reply, InfoHash, State};
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%%                                  {noreply, State, Timeout} |
%%                                  {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast(stop, State) ->
    {stop, shutdown, State};
handle_cast(_Msg, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(min_interval, State) ->
    ?LOG("minimal call interval has been reached"),
    %io:format("minimal call interval has been reached~n", []),
    {noreply, State#state{minimal_interval = none}};
handle_info(interval, State) ->
    ?LOG("interval has been reached"),
    %io:format("interval has been reached~n", []),
    {noreply, connect_to_tracker(State, "")};
handle_info({interval, Status}, State) ->
    ?LOG("interval has been reached"),
    {noreply, connect_to_tracker(State, Status)};
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, #state{connected = false}) ->
    ok;
terminate(_Reason, #state{torrent = #torrent{announce = Announce,
					     info_hash = Hash}}) ->
    ?LOG("Shutting down the tracker_server"),
    PeerId = singleton_storage:get_peerid(),
    Port = singleton_storage:get_port(),
    tracker:connect(Announce, Hash, PeerId, Port, 0, 0, 0, "stopped"),
    ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Setting up a connection to the tracker and handles the result
%% @end
%%--------------------------------------------------------------------
-spec connect_to_tracker(S :: #state{}, Event :: string()) -> #state{}.
connect_to_tracker(S = #state{minimal_interval = none,
			      peers = OldPeers,
			      torrent = #torrent{announce = Announce,
						 info_hash = Hash},
			      controller = Ctrl},
		  Event) ->
    PeerId = singleton_storage:get_peerid(),
    Port = singleton_storage:get_port(),
    Response = tracker:connect(Announce, Hash, PeerId, Port, 0, 0, 0,
			       Event),
    {Min, Interval, Con, Peers} = case Response of
				      {error, _Resason} ->
					  %Could not connect to tracker
					  {0, ?DEFAULT_INTERVAL,
					   false, []};
				      {ok, Result} ->
					  handle_connection(Result)
		      end,
    NewPeers = case Peers of
		   [] -> OldPeers;
		   Else -> Else
	       end,
    case Ctrl of
	undefined -> ok;
	_ -> torrent_ctrl:tracker_callback(Ctrl, NewPeers)
    end,
    S#state{minimal_interval = minimal_interval(Min),
	    interval = interval(Interval), connected = Con,
	    peers = NewPeers};
connect_to_tracker(State, _Event) ->
    ?LOG("Wanted to connect to tracker, but minimal interval not reached"), 
    State.

%%--------------------------------------------------------------------
%% @doc
%% Function that handles the result of a successful tracker request.
%% @end
%%--------------------------------------------------------------------
-spec handle_connection(Bencoded :: bencode:b_dictionary()) ->
					  {Min :: integer()
					  ,Interval :: integer()
					  ,Success :: boolean()
					  ,Peers :: [tracker:address()]}.
handle_connection(Bencoded) ->
    case torrent_file:get_value("failure reason", Bencoded) of
	{string, _Reason} ->
	    {0, ?DEFAULT_INTERVAL, false, []};
	{error, _} ->
	    Min = case torrent_file:get_value("min interval", Bencoded) of
		      {error, _} -> 0;
		      {integer, Value} -> Value
		  end,
	    ?LOG({minimal_interval, Min}),
	    Interval = case torrent_file:get_value("interval", Bencoded) of
			   {error, _} -> ?DEFAULT_INTERVAL;
			   {integer, Seconds} -> Seconds
		       end,
	    ?LOG({interval, Interval}),
	    Peers = case torrent_file:get_value("peers", Bencoded) of
			{error, _} -> [];
			Else -> tracker:parse_peers(Else)
		    end,
	    {Min, Interval, true, Peers}
    end.
			       

%%--------------------------------------------------------------------
%% @doc
%% Function to create a timer for notification of when the minimal
%% time before we are allowed to connect to the tracker again.
%% @end
%%--------------------------------------------------------------------
-spec minimal_interval(Interaval :: integer()) -> none | timer:tref().
minimal_interval(0) -> none;
minimal_interval(Interval) when Interval > 0 -> 
    {ok, Ref} = timer:send_after(timer:seconds(Interval),
				 min_interval),
    Ref.

%%--------------------------------------------------------------------
%% @doc
%% Function to create a timer for notification of when the tracker
%% wants us to connect again for updates.
%% @end
%%--------------------------------------------------------------------
-spec interval(Interval :: integer()) -> timer:tref().
interval(Interval) when Interval > 0 ->
    {ok, Ref} = timer:send_after(timer:seconds(Interval),
				 interval),
    Ref.

-spec interval(Interval :: integer(), State :: string()) -> timer:tref().
interval(Interval, State) when Interval > 0 ->
    {ok, Ref} = timer:send_after(timer:seconds(Interval),
				 {interval, State}),
    Ref.
